﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace SpecialEffects
{
    /// <summary>
    /// This class represents basic fire and smoke effects. It is used for damage smoke,
    /// smoke trails and fire trails.
    /// </summary>
    public class BasicFireAndSmoke : ParticleEffect
    {
        /// <summary>
        /// The size of the area where particle should be created.
        /// </summary>
        private float area
        {
            get;
            set;
        }

        public Vector2 particleSize
        {
            get;
            private set;
        }

        /// <summary>
        /// The direction of the wind.
        /// </summary>
        private Vector3 wind
        {
            get;
            set;
        }

        /// <summary>
        /// Delay time when particles becomes visible.
        /// </summary>
        private float fadeInTime
        {
            get;
            set;
        }

        /// <summary>
        /// The spreading angle of the direction.
        /// </summary>
        private float spreadAngle
        {
            get;
            set;
        }

        /// <summary>
        /// All particles lives in this array.
        /// </summary>
        private ParticleVertex[] particles
        {
            get;
            set;
        }

        public bool isActive = true;

        /// <summary>
        /// The start index of the first active particle in the particles array.
        /// </summary>
        private int activeStart
        {
            get;
            set;
        }

        /// <summary>
        /// Number of active particles in the particles array.
        /// </summary>
        private int nActive
        {
            get;
            set;
        }

        private Vector3 movingDirection
        {
            get;
            set;
        }

        /// <summary>
        /// The constructor of this class. It is private and is called from public static factory methods.
        /// </summary>
        private BasicFireAndSmoke(Texture2D tex, int particleCount, Vector2 particleSize, float maxTime, Vector3 wind, float fadeInTime, float area, float spreadAngle, float maxSpeed)
        {
            this.particleCount = particleCount;
            this.particleSize = particleSize;
            this.maxTime = maxTime;
            this.wind = wind;
            this.texture = tex;
            this.fadeInTime = fadeInTime;
            this.area = area;
            this.spreadAngle = spreadAngle;
            this.maxSpeed = maxSpeed;
            this.rand = new Random();

            hasFinished = false;

            this.device = GlobalGameClasses.GlobalGameValues.Values.graphicsDevice;

            //create vertex and index buffer to hold all particles
            deviceVertexBuffer = new VertexBuffer(device, ParticleVertex.VertexDeclaration, particleCount * 4, BufferUsage.WriteOnly);
            deviceIndexBuffer = new IndexBuffer(device, typeof(int), particleCount * 6, BufferUsage.WriteOnly);

            CreateParticleVertices();
        }
         
        #region Static factory methods for fire and smoke effects

        /// <summary>
        /// Creates the default fire trail of the player ship
        /// </summary>
        public static BasicFireAndSmoke CreateFireTrail_PlayerShip()
        {
            Texture2D fireTrailTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("fireTexture", out fireTrailTexture))
                throw new System.ArgumentException("Couldn't find fireTexture Texture");
            BasicFireAndSmoke fireTrail_basic = new BasicFireAndSmoke(fireTrailTexture, 800, new Vector2(10), 150, Vector3.Zero, 0, 1, 0, 150);
            //particleCount, particleSize, maxTime, wind, fadeInTime, area, spreadAngle, maxSpeed

            return fireTrail_basic;
        }

        /// <summary>
        /// Creates the default smoke trail of the player ship
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeTrail_PlayerShip()
        {
            Texture2D smokeTrailTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTrailTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeTrail_basic = new BasicFireAndSmoke(smokeTrailTexture, 800, new Vector2(10), 350, Vector3.Zero, 10, 1, 0, 150);

            return smokeTrail_basic;
        }

        /// <summary>
        /// Creates a smoke trail of a missile.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeTrail_Missile()
        {
            Texture2D smokeTrail_missileTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTrail_missileTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeTrail_missile = new BasicFireAndSmoke(smokeTrail_missileTexture, 600, new Vector2(3), 300, Vector3.Zero, 0, 1, 0, 50);

            return smokeTrail_missile;
        }

        /// <summary>
        /// Creates the damage smoke level 1 of the player ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel1_Player()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 300, new Vector2(25), 200, new Vector3(10, 0, 0), 0, 5, 1, 10);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 2 of the player ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel2_Player()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 400, new Vector2(25), 300, new Vector3(10, 0, 0), 0, 10, 5, 20);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 3 of the player ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel3_Player()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 500, new Vector2(25), 400, new Vector3(10, 0, 0), 0, 15, 10, 30);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 4 of the player ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel4_Player()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 600, new Vector2(25), 500, new Vector3(10, 0, 0), 0, 20, 15, 40);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 5 of the player ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel5_Player()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 700, new Vector2(25), 600, new Vector3(10, 0, 0), 0, 25, 20, 50);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 1 of the boss ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel1_Boss()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 400, new Vector2(25), 200, new Vector3(10, 0, 0), 0, 5, 1, 10);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 2 of the boss ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel2_Boss()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 500, new Vector2(25), 300, new Vector3(10, 0, 0), 0, 10, 5, 20);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 3 of the boss ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel3_Boss()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 600, new Vector2(25), 400, new Vector3(10, 0, 0), 0, 20, 10, 30);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 4 of the boss ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel4_Boss()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 700, new Vector2(25), 500, new Vector3(10, 0, 0), 0, 25, 15, 40);

            return smokeDamage;
        }

        /// <summary>
        /// Creates the damage smoke level 5 of the boss ship.
        /// </summary>
        public static BasicFireAndSmoke CreateSmokeLevel5_Boss()
        {
            Texture2D smokeTexture;
            if (!GlobalGameClasses.GlobalGameValues.Values.content.Textures.TryGetValue("smokeTexture", out smokeTexture))
                throw new System.ArgumentException("Couldn't find smokeTexture Texture");
            BasicFireAndSmoke smokeDamage = new BasicFireAndSmoke(smokeTexture, 800, new Vector2(25), 600, new Vector3(10, 0, 0), 0, 30, 20, 50);

            return smokeDamage;
        }

        #endregion

        
        /// <summary>
        /// Generates the vertices of the particles of this particle system.
        /// </summary>
        private void CreateParticleVertices()
        {
            //create vertex and index array for the n particles
            particles = new ParticleVertex[particleCount * 4];
            indices = new int[particleCount * 6];

            Vector3 z = Vector3.Zero;

            //Sets the values of each particle to default
            int x = 0;
            for (int i = 0; i < particleCount * 4; i += 4)
            {
                particles[i + 0] = new ParticleVertex(z, new Vector2(0, 0), z, 0, -maxTime);
                particles[i + 1] = new ParticleVertex(z, new Vector2(0, 1), z, 0, -maxTime);
                particles[i + 2] = new ParticleVertex(z, new Vector2(1, 1), z, 0, -maxTime);
                particles[i + 3] = new ParticleVertex(z, new Vector2(1, 0), z, 0, -maxTime);

                indices[x++] = i + 0;
                indices[x++] = i + 3;
                indices[x++] = i + 2;
                indices[x++] = i + 2;
                indices[x++] = i + 1;
                indices[x++] = i + 0;
            }
        }


        /// <summary>
        /// Set a particle in the particles array to active and give it the given data
        /// </summary>
        /// <param name="position">The position where the particle should be created.</param>
        /// <param name="direction">The direction in which the created particle should move.</param>
        public void AddParticle(Vector3 position, Vector3 direction)
        {
            //if no particles are available
            if (nActive + 4 == particleCount * 4)
                return;

            //generate direction within 15 degrees of (0,1,0)
            Vector3 offset = new Vector3(MathHelper.ToRadians(spreadAngle));
            Vector3 randAngle = direction + RandVec(-offset, offset);

            //generate position between (-400, 0, -400) and (400,0,400)
            Vector3 randPosition = RandVec(position + new Vector3(-area, 0, -area), position + new Vector3(area, 0, area));

            //generate speed between maxSpeed/2 and maxSpeed
            float randSpeed = (float)rand.NextDouble() * (maxSpeed / 2.0f) + maxSpeed;

            //at wich index this particle should be created
            int index = OffsetIndex(activeStart, nActive);
            nActive += 4;

            //determin the start time
            float birthTime = (float)(GlobalGameClasses.GlobalGameValues.Values.gameTime.TotalGameTime.TotalMilliseconds);

            //set the given parametres to the particle
            for (int i = 0; i < 4; i++)
            {
                particles[index + i].startPos = randPosition;
                particles[index + i].direction = randAngle;
                particles[index + i].speed = randSpeed;
                particles[index + i].birthTime = birthTime;
            }
        }

        /// <summary>
        /// Increases start parameter by count positions. Circles around the queue of the particle array.
        /// </summary>
        /// <param name="start">The given start index of the first active particle in the particles array.</param>
        /// <param name="count">The given count, to move the start index in the particles array.</param>
        /// <returns>The new start index of the first active particle in the particles array.</returns>
        private int OffsetIndex(int start, int count)
        {
            for (int i = 0; i < count; i++)
            {
                start++;

                if (start == particles.Length)
                {
                    start = 0;
                }
            }
            return start;
        }

        
        /// <summary>
        /// Calculates a random vector between two given vectors.
        /// </summary>
        /// <param name="min">The first given vector, should be the minimum vector.</param>
        /// <param name="max">The second given vector should be the maximum vector.</param>
        /// <returns>A random vector between the two given vectors.</returns>
        private Vector3 RandVec(Vector3 min, Vector3 max)
        {
            return new Vector3(min.X + (float)rand.NextDouble() * (max.X - min.X),
                                min.Y + (float)rand.NextDouble() * (max.Y - min.Y),
                                min.Z + (float)rand.NextDouble() * (max.Z - min.Z));
        }

        /// <summary>
        /// Draws the fire and smoke effect with a given shader.
        /// </summary>
        /// <param name="shader">The given shader, should be prepared in the View class.</param>
        public override void Draw(Effect shader)
        {
            //set vertex and index buffers
            device.SetVertexBuffer(deviceVertexBuffer);
            device.Indices = deviceIndexBuffer;

            //set effect parameters
            shader.Parameters["ParticleTexture"].SetValue(texture);
            shader.Parameters["MaxTime"].SetValue(maxTime);
            shader.Parameters["Wind"].SetValue(wind);
            shader.Parameters["Size"].SetValue(particleSize / 2.0f);
            shader.Parameters["FadeInTime"].SetValue(fadeInTime);

            //apply the effect
            foreach (EffectPass pass in shader.CurrentTechnique.Passes)
            {
                pass.Apply();
            }

            //enable blending render states
            device.BlendState = BlendState.AlphaBlend;
            device.DepthStencilState = DepthStencilState.DepthRead;

           
            //draw billboards
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, particleCount * 4, 0, particleCount * 2);

            //unset buffers
            device.SetVertexBuffer(null);
            device.Indices = null;

            //reset render states
            device.BlendState = BlendState.Opaque;
            device.DepthStencilState = DepthStencilState.Default;
        }

       
        /// <summary>
        /// Sets the position of the center of this particle effect.
        /// </summary>
        /// <param name="position">The given position where this particle effect should be created.</param>
        public void SetPosition(Vector3 position)
        {
            this.startPos = position;
        }

        /// <summary>
        /// Updates the particle array.
        /// </summary>
        public override void Update()
        {
            AddParticle(startPos, movingDirection);

            float now = (float)GlobalGameClasses.GlobalGameValues.Values.gameTime.TotalGameTime.TotalMilliseconds;

            int startIndex = activeStart;

            //for each active particle...
            for (int i = 0; i < nActive; i++)
            {
                if (startIndex == particles.Length)
                    startIndex = 0;
                //if this particle is older than its lifespan
                if (now - particles[startIndex].birthTime > maxTime)
                {
                    particles[startIndex].birthTime = -maxTime;
                    //advance active particles startposition past the particle`s index and reduce number of active particles
                    activeStart++;
                    nActive--;

                    if (activeStart == particles.Length)
                        activeStart = 0;

                }
                startIndex++;
            }
            //update vertex and index buffers
            deviceVertexBuffer.SetData<ParticleVertex>(particles);
            deviceIndexBuffer.SetData<int>(indices);
        }
        
       
        /// <summary>
        /// Checks whether this particle effect can removed from the world
        /// </summary>
        /// <returns>true, if particle effect has finished, false otherwise</returns>
        public override bool HasFinished()
        {
            if (hasFinished)
            {
                return true;
            }
            return false;
        }


        /// <summary>
        /// Sets the direction of the effect trail. In which direction the particles should move.
        /// </summary>
        /// <param name="trailDirection"></param>
        public void SetDirection(Vector3 trailDirection)
        {
            this.movingDirection = Vector3.Normalize(trailDirection);
        }


        /// <summary>
        /// Clones a smoke trail for may missiles.
        /// </summary>
        /// <returns>A cloned smoke trail.</returns>
        public BasicFireAndSmoke CloneTrail()
        {
            BasicFireAndSmoke smokeTrail_cloned = BasicFireAndSmoke.CreateSmokeTrail_Missile();
            return smokeTrail_cloned;
        }
    }
}
